package os;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

import os.kernelProcess.KernelProcess;
import os.kernelProcess.ProcessMonitorThread;
import os.kernelProcess.SchedulerThread;
import os.kernelProcess.schedulers.FCFSScheduler;
import os.kernelProcess.schedulers.RoundRobinScheduler;
import os.kernelProcess.schedulers.SPF;

/**
 * OS main class. Provides main functionalities of the OS and links between user
 * processes and scheduler.
 * 
 * @author Ahmed El-Eryan.
 * 
 */
public class MiniOS
{
	static Logger logger = Logger.getLogger(MiniOS.class);

	/**
	 * Singleton instance.
	 */
	private static MiniOS instance;

	/**
	 * Singleton get instance.
	 * 
	 * @return Singleton instance.
	 */
	public static MiniOS getInstance()
	{
		if (instance == null)
		{
			instance = new MiniOS();
		}
		return instance;
	}

	/**
	 * Private constructor to implement the Singleton design pattern.
	 */
	private MiniOS()
	{
		PropertyConfigurator.configure("log4j.properties");
		logger.info("MiniOS started");
	}

	/**
	 * Currently logged in users.
	 */
	private Hashtable<String, MiniUser> usersList = new Hashtable<String, MiniUser>();

	/**
	 * List of running processes of all users.
	 */
	private Hashtable<String, MiniProcess> processesList = new Hashtable<String, MiniProcess>();

	/**
	 * Interface to the Kernel window.
	 */
	private MiniOSView view;

	/**
	 * Scheduler of the OS.
	 */
	private SchedulerThread scheduler;

	/**
	 * Environment of the currently logged in user. It contains the processes
	 * this user can execute.
	 */
	private Properties env;

	/**
	 * Counter to generate processes IDs.
	 */
	private int processIdCounter = 0;

	/**
	 * Counter to generate threads IDs.
	 */
	private int threadIdCounter = 0;

	/**
	 * Default Kernel process. It contains all OS threads like scheduler and
	 * process monitor.
	 */
	private KernelProcess kernelProcess;

	/**
	 * Setter for the interface of Kernel window.
	 * 
	 * @param view
	 *            Interface of Kernel window.
	 */
	public void setView(MiniOSView view)
	{
		this.view = view;
	}

	/**
	 * Getter for the interface of Kernel window.
	 * 
	 * @return Interface of Kernel window.
	 */
	public MiniOSView getView()
	{
		return view;
	}

	/**
	 * Invoked when the system is booted.
	 * 
	 * @return Pid of the Kernel process
	 */
	public String boot(String schedulerStr, int quantum)
	{
		view.displayMessage("Booting OS..");
		/*
		 * Create the default OS process and default kernel user and make the
		 * necessary initializations
		 */
		MiniUser kernelUser = new MiniUser("Kernel", 0);
		usersList.put("Kernel", kernelUser);
		kernelProcess = new KernelProcess("Kernel", getNextProcessId());

		processesList.put(kernelProcess.getPid(), kernelProcess);

		ProcessMonitorThread processMonitor = new ProcessMonitorThread(
				kernelProcess, getNextThreadId());
		kernelProcess.addThread(processMonitor);
		
		if ("Round Robin".equals(schedulerStr))
		{
			scheduler = new RoundRobinScheduler(kernelProcess,
					getNextThreadId(), quantum);
			scheduler.addThread(processMonitor);
		}
		else if ("FCFS".equals(schedulerStr))
		{
			scheduler = new FCFSScheduler(kernelProcess, getNextThreadId());
		}
		else if ("SPF".equals(schedulerStr))
		{
			scheduler = new SPF(kernelProcess, getNextThreadId());
			scheduler.addThread(processMonitor);
			processMonitor.setRemainingTime(99999999);
		}
		else
		{
			scheduler = null;
		}

		processMonitor.start();
		
		kernelProcess.addThread(scheduler);
		scheduler.start();

		view.displayMessage("Booting Completed..");

		return kernelProcess.getPid();
	}

	/**
	 * Invoked when the system is restarted.
	 * 
	 * @return Pid of the Kernel process.
	 */
	public String restart()
	{
		shutDown();
		return boot(scheduler.getSchedulerName(), scheduler.getQuantum());
	}

	/**
	 * Invoked when the system is shut down.
	 */
	@SuppressWarnings("deprecation")
	public void shutDown()
	{
		Enumeration<MiniProcess> e = processesList.elements();
		while (e.hasMoreElements())
		{
			MiniProcess current = e.nextElement();
			if (!current.getPid().equals("0"))
			{
				scheduler.terminateProcess(current);
			}
			processesList.remove(current.getPid());
		}
		Enumeration<MiniThread> eThreads = kernelProcess.getThreads()
				.elements();
		while (eThreads.hasMoreElements())
		{
			MiniThread thread = eThreads.nextElement();
			if (thread.getTid() != scheduler.getTid())
			{
				scheduler.terminateThread(thread);
			}
		}
		scheduler.stop();
		processIdCounter = 0;
		threadIdCounter = 0;
		// env.clear();
		usersList.clear();
		view.refreshProcessesList();
		MiniUIManager.getInstance().closeAllWindows();
	}

	/**
	 * Checks the validation of the log in details. If they are valid, it
	 * initializes the necessary resources.
	 * 
	 * @param username
	 *            Name of the log in user.
	 * @param password
	 *            Password of the log in user.
	 * @return True if the log in was successful. False if the log in details
	 *         are in valid.
	 */
	// tested
	public boolean userLogin(String username, String password)
	{
		// XXX Done: Develop a way to check log in details. Make any
		// assumptions.
		Properties users = new Properties();
		try
		{
			users.load(new FileReader("res/users/accounts.txt"));
			String userPassword = users.getProperty(username);
			System.out.println(userPassword + " " + password);
			if (userPassword == null || !password.equals(userPassword))
			{
				System.out.println("User " + username + " Logged In failed");
				return false;
			}
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}

		loadEnvironment(username);

		view.displayMessage("User " + username + " Logged In");
		System.out.println("User " + username + " Logged In");
		logger.info("User " + username + " logged in");
		return true;
	}

	/**
	 * <b>UnImplemented</b>: Invoked when the user logs out.
	 * 
	 * @param Name
	 *            of the logged out user.
	 */
	public void userLogout(String username)
	{

	}

	/**
	 * Loads environment file of the given user
	 * 
	 * @param user
	 *            User name.
	 */
	private void loadEnvironment(String user)
	{
		env = new Properties();
		try
		{
			env.load(new FileReader("res/" + user + "_env.properties"));
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	/**
	 * Retrieves the class path of the given process command using the loaded
	 * environment file. The process command is entered by the user in the
	 * window console.
	 * 
	 * @param processCommand
	 *            Command entered by the user to run a process.
	 * @return Class path of the process to run, or null if the processCommand
	 *         is not found in the environment file.
	 */
	@SuppressWarnings("unused")
	private String getProcessClassPath(String processCommand)
	{
		return env.getProperty(processCommand);
	}

	/**
	 * Invoked when the user enters a command in the Kernel window. If the
	 * command was shutdown, restart or logOut it should be handled specially.
	 * 
	 * @param username
	 *            name of the user that entered the command.
	 * @param command
	 *            Entered string.
	 */
	public void receiveUserCommand(String username, String command)
	{
		// parse command
		String[] processArguments = parseCommand(command);

		if ("shutdown".equalsIgnoreCase(processArguments[0]))
		{
			shutDown();
		}
		else if ("restart".equalsIgnoreCase(processArguments[0]))
		{
			restart();
		}
		else if ("logOut".equalsIgnoreCase(processArguments[0]))
		{
			userLogout(username);
		}
		else
		{
			System.out.println("abl create process");
			createProcess(username, processArguments);
		}
	}

	/**
	 * Parses the command of the user into tokens using Regex.
	 * 
	 * @param command
	 *            Command entered by the user.
	 * @return Tokens of the command.
	 */
	private String[] parseCommand(String command)
	{
		return command.split("\\s");
	}

	/**
	 * <b>UnImplemented</b>: Creates a new process instance using the command
	 * entered by the user. The steps followed to create a process are:
	 * <ol>
	 * <li>Instantiate the right process (Hint: get the class path from the
	 * environment file, use java.lang.reflect package to create an object using
	 * the Class path)</li>
	 * <li>Give process a PID.</li>
	 * <li>Add the process to processesList.</li>
	 * <li>Inform the MiniUIManager that a process is created (by calling the
	 * processCreated method).</li>
	 * <li>Create process main thread and start it.</li>
	 * </ol>
	 * 
	 * @param username
	 *            Name of the user that entered the command.
	 * @param processArguments
	 *            The result of parsing the user command (tokens of the
	 *            command).
	 */
	// tested
	@SuppressWarnings("unchecked")
	public void createProcess(String username, String[] processArguments)
	{
		/*
		 * In create process you should: 1) Instantiate the right process (Hint:
		 * get the class path from the environment file, use java.lang.reflect
		 * package to create an object using the Class path) 2) Give process a
		 * PID 3) Add the process to processesList 4) Inform the MiniUIManager
		 * that a process is created (by calling the processCreated method) 3)
		 * Create process main thread and start it
		 */
		// XXX Done MiniOS::createProcess
		String classPath = env.getProperty(processArguments[0]);
		System.out.println(classPath);
		try
		{
			Class<MiniProcess> classObj = (Class<MiniProcess>) Class
					.forName(classPath);
			Constructor<MiniProcess> cons = classObj.getConstructor(
					String.class, String.class);
			MiniProcess process = cons.newInstance(username, ++processIdCounter
					+ "");
			processesList.put("" + processIdCounter, process);
			logger.info("Process " + process.name + " of PID "
					+ processIdCounter + "is created");
			view.displayMessage("Process " + process.name + " of PID "
					+ processIdCounter + "is created");
			MiniUIManager.getInstance().processCreated("" + processIdCounter,
					"Process" + processIdCounter);
			process.createMainThread(processArguments).start();
		}
		catch (ClassNotFoundException e)
		{
			e.printStackTrace();
		}
		catch (SecurityException e)
		{
			e.printStackTrace();
		}
		catch (NoSuchMethodException e)
		{
			e.printStackTrace();
		}
		catch (IllegalArgumentException e)
		{
			e.printStackTrace();
		}
		catch (InstantiationException e)
		{
			e.printStackTrace();
		}
		catch (IllegalAccessException e)
		{
			e.printStackTrace();
		}
		catch (InvocationTargetException e)
		{
			e.printStackTrace();
		}
	}

	/**
	 * <b>UnImplemented</b>: A process that desires to create a thread must call
	 * this method. Steps for creating a thread:
	 * <ol>
	 * <li>Instantiate an object using the thread class path provided (use
	 * java.lang.reflect package).</li>
	 * <li>Pass the arguments to the created thread.</li>
	 * <li>Add the thread to the process.</li>
	 * <li>Add the thread to the scheduler.</li>
	 * </ol>
	 * 
	 * @param process
	 *            Owning process
	 * @param threadClassPath
	 *            Path of the thread class.
	 * @param arguments
	 *            Console arguments in case the created thread is the process
	 *            main thread, null otherwise.
	 * @return The created thread.
	 */
	// tested
	@SuppressWarnings("unchecked")
	public MiniThread createThread(String process, String threadClassPath,
			Object[] arguments)
	{
		// XXX Done MiniOS::createThread
		Class<MiniThread> classObj;
		MiniThread thread = null;
		try
		{
			Properties remainingTime = new Properties();
			remainingTime.load(new FileInputStream("res/SPF_average.properties"));
			int remainingT = 0;
			String s;
			if ((s = remainingTime.getProperty(threadClassPath)) != null)
				remainingT = Integer.parseInt(s);
			remainingTime.clear();
			classObj = (Class<MiniThread>) Class.forName(threadClassPath);
			Constructor<MiniThread> cons = classObj.getConstructor(
					MiniProcess.class, String.class);
			MiniProcess threadProcess = processesList.get(process.toString());
			thread = cons.newInstance(threadProcess, ++threadIdCounter + "");
			thread.passThreadArguments(arguments);
			thread.setRemainingTime(remainingT);
			threadProcess.addThread(thread);
			scheduler.addThread(thread);
			logger.info("Thread " + thread.tid + " belonging to process "
					+ thread.process.pid + "is created");
			view.displayMessage("Thread " + thread.tid
					+ " belonging to process " + thread.process.pid
					+ "is created");
		}
		catch (ClassNotFoundException e)
		{
			e.printStackTrace();
		}
		catch (SecurityException e)
		{
			e.printStackTrace();
		}
		catch (NoSuchMethodException e)
		{
			e.printStackTrace();
		}
		catch (IllegalArgumentException e)
		{
			e.printStackTrace();
		}
		catch (InstantiationException e)
		{
			e.printStackTrace();
		}
		catch (IllegalAccessException e)
		{
			e.printStackTrace();
		}
		catch (InvocationTargetException e)
		{
			e.printStackTrace();
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		return thread;
	}

	/**
	 * Gives the ID of the next process to execute.
	 * 
	 * @return ID of the next process to execute.
	 */
	private String getNextProcessId()
	{
		return processIdCounter++ + "";
	}

	/**
	 * Gives the ID of the next thread to run.
	 * 
	 * @return ID of the next thread to run.
	 */
	private String getNextThreadId()
	{
		return threadIdCounter++ + "";
	}

	/**
	 * Invoked when the user chooses to suspend the given process.
	 * 
	 * @param process
	 *            Process to be suspended.
	 */
	public void suspendProcess(MiniProcess process)
	{
		scheduler.suspendProcess(process);
	}

	/**
	 * Invoked when the user chooses to resume a suspended process.
	 * 
	 * @param process
	 *            Process to be resumed.
	 */
	public void resumeProcess(MiniProcess process)
	{
		scheduler.resumeProcess(process);
	}

	/**
	 * Invoked when the user closes the application by closing its window.
	 * 
	 * @param process
	 *            Id of the process to be terminated.
	 */
	public void terminateProcess(String process)
	{
		terminateProcess(processesList.get(process));
	}

	/**
	 * Invoked when the user chooses to terminate the given process from the
	 * Process Monitor tab.
	 * 
	 * @param process
	 *            Process to be terminated.
	 */
	public void terminateProcess(MiniProcess process)
	{
		processTerminated(process.getPid());
		scheduler.terminateProcess(process);
	}

	/**
	 * Called when all the threads of a process are terminated. It removed the
	 * process from the system.
	 * 
	 * @param pid
	 *            Process to be removed.
	 */
	public void processTerminated(String pid)
	{
		MiniProcess terminated = processesList.remove(pid);
		view.displayMessage("Process " + terminated.getName() + " of PID "
				+ pid + " is Terminated");
		MiniUIManager.getInstance().processTerminated(pid);
	}

	/**
	 * Invoked when the user chooses to terminate the given thread.
	 * 
	 * @param thread
	 *            Thread to be terminated.
	 */
	public void terminateThread(MiniThread thread)
	{
		thread.getProcess().threadTerminated(thread.getTid());
		scheduler.terminateThread(thread);
		if (processesList.get(thread.getProcess().getPid()).getThreadCount() == 0)
		{
			processTerminated(thread.getProcess().getPid());
		}
	}

	/**
	 * Invoked when the user chooses to suspended the given thread.
	 * 
	 * @param thread
	 *            Thread to be suspended.
	 */
	public void suspendThread(MiniThread thread)
	{
		scheduler.suspendThread(thread);
	}

	/**
	 * Invoked when the user chooses to resume a suspended thread.
	 * 
	 * @param thread
	 *            Thread to be resumed.
	 */
	public void resumeThread(MiniThread thread)
	{
		scheduler.resumeThread(thread);
	}

	/**
	 * Invoked when a thread terminates. (At the end of a thread run() method)
	 * 
	 * @param pid
	 *            Pid of the owner process.
	 * @param tid
	 *            Id of the terminated thread.
	 */
	public void threadTerminated(String pid, String tid)
	{
		processesList.get(pid).threadTerminated(tid);
		scheduler.threadTerminated();
		if (processesList.get(pid).getThreadCount() == 0)
		{
			processTerminated(pid);
		}
	}

	/**
	 * Getter for the processes list.
	 * 
	 * @return List of processes in the system.
	 */
	public Hashtable<String, MiniProcess> getProcessesList()
	{
		return processesList;
	}

	/**
	 * Gets the process with the given ID.
	 * 
	 * @param pid
	 *            Id of the process required.
	 * @return Process whose ID is pid.
	 */
	public MiniProcess getProcess(String pid)
	{
		return processesList.get(pid);
	}

	/**
	 * <b>UnImplemented</b>: Invoked by a thread request to display a message on
	 * the process window.
	 * 
	 * @param thread
	 *            Thread making the request.
	 * @param message
	 *            String to be displayed on the process window.
	 */
	// tested
	public void outputRequest(MiniThread thread, String message)
	{
		// XXX:Done Implement MiniOS::outputRequest()
		logger.info("Thread " + thread.getName() + " with Id = "
				+ thread.getId() + " started output request");
		scheduler.blockThread();
		System.out.print("ana ba3mal desplay lel message");
		MiniUIManager.getInstance().displayMessage(thread.getProcessId(),
				thread.getTid(), message);
	}

	/**
	 * <b>UnImplemented</b>: Invoked when the output request made by the given
	 * thread is completed by displaying the message on the process window.
	 * 
	 * @param pid
	 *            Processes owning the thread that made the request.
	 * @param tid
	 *            Thread that made the request.
	 */
	// tested
	public void outputRequestCompleted(String pid, String tid)
	{
		// XXX Done MiniOS::outputRequestCompleted
		logger.info("Thread " + tid + " finished output request");
		MiniProcess process = processesList.get(pid);
		if (process != null)
		{
			MiniThread thread = process.getThread(tid);
			if (thread != null)
				scheduler.unblockThread(thread);
		}
		
	}

	/**
	 * <b>UnImplemented</b>: Invoked by a thread requesting an input from the
	 * user.
	 * 
	 * @param thread
	 *            Thread request the input.
	 */
	// tested
	public void inputRequest(MiniThread thread)
	{
		// XXX: Done MiniOS::inputRequest()
		logger.info("Thread " + thread.getName() + " with Id = "
				+ thread.getId() + " started input request");
		scheduler.blockThread();
		thread.getProcess().inputRequest(thread);
	}

	/**
	 * <b>UnImplemented</b>: Invoked when the user inputs a string in response
	 * to an input request made by the given thread.
	 * 
	 * @param pid
	 *            Pid of the process owning the thread that made the input
	 *            request.
	 * @param tid
	 *            Id of the thread that made the request.
	 * @param input
	 *            String input by the user.
	 */
	// tested
	public void inputRequestCompleted(String pid, String tid, String input)
	{
		// XXX: Done MiniOS::inputRequestCompleted()
		logger.info("Thread " + tid + " finished input request");
		MiniProcess process = processesList.get(pid);
		process.inputRequestCompleted();
		MiniThread thread = process.getThread(tid);
		thread.setInput(input);
		scheduler.unblockThread(thread);
	}

	/**
	 * Joins the parent thread to the shouldWait thread i.e the parent thread
	 * waits until shouldWait thread finishes execution
	 * 
	 * @param parent
	 * @param shouldWait
	 */
	// bonus tested
	public void join(MiniThread parent, MiniThread shouldWait)
	{
		// TODO, make it MiniMutex
		shouldWait.addJoin(parent);
		scheduler.blockThread();
	}

	public void unblockThread(MiniThread thread)
	{
		scheduler.unblockThread(thread);
	}

	public void threadStopped(MiniThread thread)
	{
		scheduler.threadStopped(thread);
	}
}
